#include "Philosophers.h"

// Constructor. Main window creation and setup
Philosophers::Philosophers()
{
	// Setting up the main window
	CtrlLayout(*this, "Philosophers");
	MinimizeBox();
	AddFrame(menu);
	menu.Set(THISBACK(MainMenu));
	AddFrame(status);
	menu.WhenHelp = status;
	phTable.AddColumn("#");
	phTable.AddColumn("Name");
	phTable.AddColumn("Method");
	phTable.AddColumn("Status");
	phTable.AddColumn("Left");
	phTable.AddColumn("Right");
	phTable.AddColumn("Hunger");
	phTable.AddColumn("Fork");
	WhenClose = THISBACK(Exit);
	// Setting initial values
	speed = 500;
	maxHunger = 0; // off
	regHunger = 0;
	stopDead = true;
	running = false;
	logFile = "phils.log";
	// Message
	Log("The application has been started");
}

// Creates main menu
void Philosophers::MainMenu(Bar &bar)
{
	bar.Add("Modeling", THISBACK(GameMenu));
	bar.Add("Help", THISBACK(HelpMenu));
}

// Creates menu "Modeling"
void Philosophers::GameMenu(Bar &bar)
{
	bar.Add("Start", THISBACK(NewGame)).Help("Select modeling parameters and start the process");
	bar.Add("Stop", THISBACK(Stop)).Help("Stop modeling process to study the log");
	bar.Add("Exit", THISBACK(Exit)).Help("Quit the application");
}

// Creates "Help" menu
void Philosophers::HelpMenu(Bar &bar)
{
	bar.Add("Help", THISBACK(Help)).Help("Display help file");
	bar.Add("About", THISBACK(About)).Help("About this application");
}

// Collects information to start a new game
void Philosophers::NewGame()
{
	if(running)
		Stop();
	ng.Link(&speed, &maxHunger, &stopDead, &phils, &logFile);
	switch(ng.Run(true))
	{
		case IDOK:
			Start();
			break;
		case IDCANCEL:
			// Nothing();-)
			break;
	}
}

// Starts modeling process
void Philosophers::Start()
{
	running = true;
	regHunger = 0;
	if(!log.Open(logFile))
	{
		Exclamation("Error: could not open log file for writing!");
		NewGame();
		return;
	}
	Log("Starting a new modeling process");
	// Linking our "round table" and correctign the parameters
	int i, lim = phils.GetCount() - 1;
	for(i = 0; i <= lim; i++)
	{
		phils[i].SetId(i);
		i == 0 ? phils[i] >> phils[lim] : phils[i] >> phils[i - 1];
		i == lim ? phils[i] << phils[0] : phils[i] << phils[i + 1];
		phils[i].Log = callback(this, &Philosophers::Log);
		phils[i].Update = callback(this, &Philosophers::Update);
		phils[i].UpdateFork = callback(this, &Philosophers::UpdateFork);
	}
	// Making waiting periods vector
	if(waiting.GetCount() > 0)
		waiting.Clear();
	for(i = 0; i <= lim; i++)
		waiting.Add(Time::Low());
	// Making the table that displays the modeling in real-time
	phTable.Clear();
	String algo;
	for(i = 0; i <= lim; i++)
	{
		switch(phils[i].GetAlgo())
		{
			case Simple:
				algo = "Simple";
				break;
			case Random:
				algo = "Random";
				break;
			case Ordered:
				algo = "Ordered";
				break;
			case Voluntary:
				algo = "Voluntary";
				break;
		}
		phTable.Add(i, phils[i].GetName(), algo, "Missing", "No", "No", 0, "+");
	}
	// Launching the execution threads
	for(i = 0; i <= lim; i++)
		phils[i].Run();
}

// Stopping the modeling
void Philosophers::Stop()
{
	Log(String("Max. hunger level during modeling: ") + String(AsString(regHunger)));
	Log("Modeling is being stopped");
	running = false;
	// Stopping the threads
	int i, lim = phils.GetCount() - 1;
	for(i = 0; i <= lim; i++)
		phils[i].Stop();
	Thread::Sleep(500);
	for(i = 0; i <= lim; i++)
		phils[i].Terminate();
	// Stopping the logging
	Log("The modeling has been stopped");
	log.Close();
}

// Quit the application
void Philosophers::Exit()
{
	if(PromptOKCancel("Are you sure want to quit?"))
	{
		if(running)
			Stop();
		Break();
	}
}

// Help display
void Philosophers::Help()
{
	help.GoTo("topic://Philosophers/app/main$en-us");
	help.Execute();
}

// About display
void Philosophers::About()
{
	if(!about.IsOpen())
		about.Open();
	else
		about.Close();
}

// Adding a new record to logfile
void Philosophers::Log(String msg)
{
	msg = String("[") + AsString(GetSysTime()) + String("] ") + msg + String("\n");
	log.Put(msg);
	if(logBox.GetLineCount() > 300)
		logBox.Clear();
	logBox.Insert(logBox.GetLength(), msg);
	logBox.ScrollDown();
}

// Detecting "neighbor plot"
void Philosophers::CheckPlot(int id)
{
	if(waiting[id] > Time::Low())
	{
		int mul = 2 + (1010 - speed) / 100;
		if((GetSysTime() - waiting[id]) > ((Person::maxAppetite + Person::rndFactor) * mul) && running)
		{
			Log(phils[id].GetName() + String(": awaiting period has been exceeded. Possible situation <Neighbor plot>"));
			if(stopDead)
			{
				Stop();
				PromptOK("Detected deadlock <Neighbor plot>. The modeling has been stopped.");
			}
		}
	}
}

// Detecting "starvation"
void Philosophers::CheckDeadlock()
{
	// Check twice to avoid the main thread delay effect
	bool deadlock = true;
	int i, j;
	for(i = 0; i < 2; i++)
	{
		for(j = 0; j < phils.GetCount(); j++)
		{
			if(phTable.Get(j, 3).ToString() != "Ждет" || phTable.Get(j, 6) == 0 || (phTable.Get(j, 4).ToString() == "Да" && phTable.Get(j, 5).ToString() == "Да"))
			{
				deadlock = false;
				break;
			}
		}
		if(!deadlock) break;
	}
	if(deadlock && running)
	{
		Log("Detected deadlock <Starvation>");
		if(stopDead)
		{
			Stop();
			PromptOK("Detected deadlock <Starvation>. The modeling has been stopped.");
		}
	}
}

// Detecting max. hunger level exceeding
void Philosophers::CheckHunger(int id, int hunger)
{
	if(hunger > regHunger)
		regHunger = hunger;
	if(maxHunger > 0 && hunger > maxHunger && running)
	{
		Log(phils[id].GetName() + String(": starvation threshold exceeded. Possible situation <Neighbor plot>"));
		if(stopDead)
		{
			Stop();
			PromptOK("Starvation threshold exceeded. The modeling has been stopped.");
		}
	}
}

// Updates table row
void Philosophers::Update(int id, PersonInfo info)
{
	// Checking status and making string representation
	String state;
	switch(info.state)
	{
		case Thinking:
			state = "Meditation";
			break;
		case Eating:
			state = "Dining";
			if(waiting[id] > Time::Low())
				waiting[id] = Time::Low();
			break;
		case Waiting:
			state = "Waiting";
			if(waiting[id] == Time::Low())
				waiting[id] = GetSysTime();
			break;
	}
	// Updating table row
	phTable.Set(id, 3, state);
	phTable.Set(id, 4, info.lfork ? "Yes" : "No");
	phTable.Set(id, 5, info.rfork ? "Yes" : "No");
	phTable.Set(id, 6, info.hunger);
	phTable.Set(id, 7, info.fork >= 0 ? AsString(info.fork) : String("+"));
	// Checking deadlocks
	CheckPlot(id);
	// Starvation is a stable situation, so checking it every 2N updates
	static int cnt = 0;
	if(cnt == phils.GetCount()*2)
	{
		CheckDeadlock();
		cnt = 0;
	}
	else
		cnt++;
	// Register max hunger and check starvation threshold
	CheckHunger(id, info.hunger);
}

// Updating only fork holder's index
void Philosophers::UpdateFork(int id, int holder)
{
	phTable.Set(id, 7, holder >= 0 ? AsString(holder) : String("+"));
}

// Application main entry point
GUI_APP_MAIN
{
	Philosophers().Run();
}

